﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text;
using Pahan.McuFusion.Common;

namespace Pahan.McuFusion.FusionBoot
{
    [Serializable]
    internal class FusionBootProgrammer : IProgrammer
    {
        public FusionBootProgrammer(FusionBootMcuInfo mcu, FusionBootProgrammerInfo info, IFusionBootAdapter adapter)
        {
            this.Info = info;
            this.mcu = mcu;
            this.adapter = adapter;
        }

        #region Implementation of IDisposable

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        /// <filterpriority>2</filterpriority>
        public void Dispose()
        {
            Close();
            adapter.Dispose();
        }

        #endregion

        #region Implementation of IProgrammer

        /// <summary>
        /// Gets the information about the programmer.
        /// </summary>
        public ProgrammerInfo Info
        {
            get;
            private set;
        }

        /// <summary>
        /// Opens the programmer. This method must be called before the actual use of the programmer.
        /// </summary>
        public void Open()
        {
            lock (syncRoot)
            {
                worker = new BackgroundWorker();
                worker.WorkerReportsProgress = true;
                worker.WorkerSupportsCancellation = true;
                worker.ProgressChanged += (s, e) =>
                {
                    var handler = ProgressChanged;
                    if (handler != null)
                    {
                        handler(this, e);
                    }
                };
                adapter.Open();
                adapter.WriteByte((byte)Command.Enable);
                Result result = (Result)adapter.ReadByte();
                if (result != Result.OK)
                {
                    adapter.DiscardInputBuffer();
                    adapter.WriteByte((byte)Command.Enable);
                    result = (Result)adapter.ReadByte();
                    if (result != Result.OK)
                    {
                        throw new Exception("Cannot open FusionBoot device.");
                    }
                }
                
                CheckFusionBootVersion();

                CheckDevice();
            }
        }

        private void CheckFusionBootVersion()
        {
            adapter.WriteByte((byte)Command.GetFusionInfo);
            Result result = (Result)adapter.ReadByte();
            if (result != Result.OK)
            {
                throw new InvalidOperationException();
            }
            byte length = adapter.ReadByte();
            byte[] data = new byte[length];
            adapter.ReadWithControlSum(data);
            var info = FusionBootInfo.Create(data);
            if (info.ProtocolVersion.Major != 1)
            {
                throw new NotSupportedException(String.Format(
                    "The programmer supports only 1.x version of the Fusion Boot protocol while the slave device uses version {0}.",
                    info.ProtocolVersion.ToString(2)));
            }
        }

        private void CheckDevice()
        {
            adapter.WriteByte((byte)Command.GetDeviceInfo);
            Result result = (Result)adapter.ReadByte();
            if (result != Result.OK)
            {
                throw new InvalidOperationException();
            }
            byte length = adapter.ReadByte();
            byte[] data = new byte[length];
            adapter.ReadWithControlSum(data);
            if (!data.SequenceEqual(mcu.ModelCode))
            {
                throw new InvalidOperationException("Invalid device.");
            }
        }

        /// <summary>
        /// Closes the programmer.
        /// </summary>
        public void Close()
        {
            lock (syncRoot)
            {
                if (worker != null)
                {
                    if (worker.IsBusy)
                    {
                        throw new InvalidOperationException("Cannot close the programmer while performing operation.");
                    }
                    worker.Dispose();
                }
                adapter.Close();
            }
        }

        /// <summary>
        /// Cancels the current asynchronous operation.
        /// </summary>
        /// <seealso cref="IProgrammer.EraseMemoryAsync"/>
        /// <seealso cref="IProgrammer.ReadMemoryAsync"/>
        /// <seealso cref="IProgrammer.WriteMemoryAsync"/>
        public void CancelAsync()
        {
            worker.CancelAsync();
        }

        /// <summary>
        /// Erases the memory. This method does not block the calling thread.
        /// </summary>
        /// <param name="memory">The memory to erase.</param>
        /// <seealso cref="IProgrammer.AsyncCompleted"/>
        public void EraseMemoryAsync(MemoryClassInfo memory)
        {
            worker.DoWork += DoEraseMemory;
            worker.RunWorkerCompleted += worker_RunWorkerCompleted;
            worker.RunWorkerAsync(new AsyncArgument(memory));
        }

        /// <summary>
        /// Reads the program memory. This method does not block the calling thread.
        /// </summary>
        /// <param name="output">Output stream.</param>
        /// <param name="memory">The memory to read.</param>
        /// <seealso cref="IProgrammer.AsyncCompleted"/>
        public void ReadMemoryAsync(Stream output, MemoryClassInfo memory)
        {
            worker.DoWork += DoReadProgramMemory;
            worker.RunWorkerCompleted += worker_RunWorkerCompleted;
            worker.RunWorkerAsync(new AsyncArgument(output, memory));
        }

        /// <summary>
        /// Writes the program memory. This method does not block the calling thread.
        /// </summary>
        /// <param name="input">Input stream.</param>
        /// <param name="memory">The memory to write.</param>
        /// <seealso cref="IProgrammer.AsyncCompleted"/>
        public void WriteMemoryAsync(Stream input, MemoryClassInfo memory)
        {
            worker.DoWork += DoWriteProgramMemory;
            worker.RunWorkerCompleted += worker_RunWorkerCompleted;
            worker.RunWorkerAsync(new AsyncArgument(input, memory));
        }

        public event AsyncCompletedEventHandler AsyncCompleted;
        public event ProgressChangedEventHandler ProgressChanged;

        #endregion

        #region Private Members

        private void DoEraseMemory(Object sender, DoWorkEventArgs e)
        {
            var arg = (AsyncArgument)e.Argument;
            for (ushort page = 0; page < arg.Memory.PageCount; page++)
            {
                if (worker.CancellationPending)
                {
                    e.Cancel = true;
                    break;
                }
                byte cmd = GetMemoryCommand(Command.EraseMemory, arg.Memory);
                adapter.WriteByte(cmd);
                Result result = (Result)adapter.ReadByte();
                if (result != Result.OK)
                {
                    throw new InvalidOperationException();
                }
                var data = new byte[] 
                {
                    (byte)page, 
                    (byte)(page >> 8)
                };
                adapter.WriteWithControlSum(data, cmd);
                result = (Result)adapter.ReadByte();
                switch (result)
                {
                    case Result.OK:
                        break;
                    case Result.NonWritable:
                        break; // Just Skip.
                    case Result.NotSupported:
                        throw new NotSupportedException("The operation is not supported.");
                    default:
                        throw new InvalidOperationException();
                }
                worker.ReportProgress(page * 100 / arg.Memory.PageCount);
            }
        }

        private void DoReadProgramMemory(Object sender, DoWorkEventArgs e)
        {
            var arg = (AsyncArgument)e.Argument;
            for (ushort page = 0; page < arg.Memory.PageCount; page++)
            {
                if (worker.CancellationPending)
                {
                    e.Cancel = true;
                    break;
                }
                byte cmd = GetMemoryCommand(Command.ReadMemory, arg.Memory);
                adapter.WriteByte(cmd);
                Result result = (Result)adapter.ReadByte();
                if (result != Result.OK)
                {
                    throw new InvalidOperationException();
                }
                var data = new [] 
                {
                    (byte)page, 
                    (byte)(page >> 8)
                };
                adapter.WriteWithControlSum(data, cmd);
                result = (Result)adapter.ReadByte();
                if (result != Result.OK)
                {
                    throw new InvalidOperationException();
                }
                byte[] bytes = new byte[arg.Memory.PageSize];
                adapter.ReadWithControlSum(bytes);
                arg.Stream.Write(bytes, 0, bytes.Length);
                worker.ReportProgress(page * 100 / arg.Memory.PageCount);
            }
        }

        private void DoWriteProgramMemory(Object sender, DoWorkEventArgs e)
        {
            var arg = (AsyncArgument)e.Argument;
            for (ushort page = 0; page < arg.Memory.PageCount; page++)
            {
                if (worker.CancellationPending)
                {
                    e.Cancel = true;
                    break;
                }
                byte cmd = GetMemoryCommand(Command.WriteMemory, arg.Memory);
                adapter.WriteByte(cmd);
                Result result = (Result)adapter.ReadByte();
                if (result != Result.OK)
                {
                    throw new InvalidOperationException();
                }
                var data = new List<byte> 
                {
                    (byte)page, 
                    (byte)(page >> 8)
                };
                var buf = new byte[arg.Memory.PageSize];
                int len = arg.Stream.Read(buf, 0, buf.Length);
                if (len < buf.Length)
                {
                    for (int i = len; i < buf.Length; i++)
                        buf[i] = arg.Memory.EmptyValue;
                }
                data.AddRange(buf);
                adapter.WriteWithControlSum(data, cmd);
                result = (Result)adapter.ReadByte();
                switch (result)
                {
                    case Result.OK:
                        break;
                    case Result.NonWritable:
                        break; // Just skip.
                    case Result.NotSupported:
                        throw new NotSupportedException("The operation is not supported.");
                    case Result.WriteError:
                        throw new Exception("Written with errors.");
                    default:
                        throw new InvalidOperationException();
                }
                worker.ReportProgress(page * 100 / arg.Memory.PageCount);
            }
        }

        private void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            var handler = AsyncCompleted;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        private byte GetMemoryCommand(Command command, MemoryClassInfo memory)
        {
            int index = mcu.Memory.IndexOf(memory);
            byte value = (byte)command;
            switch (index)
            {
                case 0:
                    return value;
                case 1:
                    return (byte)(value | 0x03);
                case 2:
                    return (byte)(value | 0x0C);
                case 3:
                    return (byte)(value | 0x0F);
                default:
                    throw new InvalidOperationException();
            }
        }

        private readonly FusionBootMcuInfo mcu;

        private readonly IFusionBootAdapter adapter;

        [NonSerialized]
        private BackgroundWorker worker;

        private readonly Object syncRoot = new Object();

        #endregion

        #region Nested Classes

        private class AsyncArgument
        {
            public AsyncArgument(MemoryClassInfo memory)
                : this(null, memory)
            {
            }

            public AsyncArgument(Stream stream, MemoryClassInfo memory)
            {
                this.Stream = stream;
                this.Memory = memory;
            }
            
            public Stream Stream
            {
                get;
                set;
            }

            public MemoryClassInfo Memory
            {
                get;
                set;
            }
        }

        #endregion
    }
}
